/*
 * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
 * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Custom;
using QuantConnect.Data.Custom.IconicTypes;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Python;
using QuantConnect.Securities;
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.ToolBox;
using QuantConnect.ToolBox;
using QuantConnect.Util;
using QuantConnect.Indicators;
using QuantConnect.Statistics;

namespace QuantConnect.Tests.Python
{
    [TestFixture]
    public partial class PandasConverterTests
    {
        private PandasConverter _converter = new PandasConverter();
        private bool _newerPandas;

        private static bool IsNewerPandas()
        {
            bool newPandas;
            using (Py.GIL())
            {
                var pandas = Py.Import("pandas");
                using var local = new PyDict();
                local["pd"] = pandas;
                newPandas = PythonEngine.Eval("int(pd.__version__.split('.')[0]) >= 1", locals: local).As<bool>();
            }
            return newPandas;
        }

        [SetUp]
        public void Setup()
        {
            SymbolCache.Clear();
            _newerPandas = IsNewerPandas();
        }

        [TearDown]
        public void TearDown()
        {
            SymbolCache.Clear();
        }

        [Test]
        public void HandlesBaseDataCollection([Values] bool flatten)
        {
            var converter = new PandasConverter();
            var data = new[]
            {
                new EnumerableData
                {
                    Data = new List<BaseData>
                    {
                        new TradeBar(new DateTime(2020, 1, 2), Symbols.IBM, 101m, 102m, 100m, 101m, 10m),
                        new TradeBar(new DateTime(2020, 1, 3), Symbols.IBM, 101m, 102m, 100m, 101m, 20m),
                    },
                    Symbol = Symbols.IBM,
                    Time = new DateTime(2020, 1, 1)
                }
            };

            dynamic dataFrame = converter.GetDataFrame(data, flatten: flatten);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                if (flatten)
                {
                    var indexNames = dataFrame.index.names.AsManagedObject(typeof(string[]));
                    CollectionAssert.AreEqual(new[] { "time", "symbol" }, indexNames);

                    Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                    var count = dataFrame.__len__().AsManagedObject(typeof(int));
                    Assert.AreEqual(2, count);
                    AssertFlattenBaseDataCollectionDataFrameTimes(data, dataFrame);
                }
                else
                {
                    var subDataFrame = dataFrame.loc[Symbols.IBM];
                    Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                    var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                    Assert.AreEqual(1, count);
                }
            }
        }

        [Test]
        public void HandlesBaseDataCollectionWithMultipleSymbols()
        {
            var converter = new PandasConverter();
            var data = new[]
            {
                new EnumerableData
                {
                    Data = new List<BaseData>
                    {
                        new TradeBar(new DateTime(2020, 1, 2), Symbols.IBM, 101m, 102m, 100m, 101m, 10m),
                        new TradeBar(new DateTime(2020, 1, 3), Symbols.IBM, 101m, 102m, 100m, 101m, 20m),
                    },
                    Symbol = Symbols.IBM,
                    Time = new DateTime(2020, 1, 1)
                },
                new EnumerableData
                {
                    Data = new List<BaseData>
                    {
                        new TradeBar(new DateTime(2020, 1, 2), Symbols.SPY, 201m, 202m, 200m, 201m, 10m),
                        new TradeBar(new DateTime(2020, 1, 3), Symbols.SPY, 201m, 202m, 200m, 201m, 20m),
                        new TradeBar(new DateTime(2020, 1, 4), Symbols.SPY, 201m, 202m, 200m, 201m, 20m),
                    },
                    Symbol = Symbols.SPY,
                    Time = new DateTime(2020, 1, 1)
                }
            };

            dynamic dataFrame = converter.GetDataFrame(data, flatten: true);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var indexNames = dataFrame.index.names.AsManagedObject(typeof(string[]));
                CollectionAssert.AreEqual(new[] { "collection_symbol", "time", "symbol" }, indexNames);

                Assert.Multiple(() =>
                {
                    foreach (var collection in data)
                    {
                        var symbol = collection.Symbol;

                        var subDataFrame = dataFrame.loc[symbol];
                        Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)), $"Empty sub-dataframe for {symbol}");

                        var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                        Assert.AreEqual(collection.Data.Count, count, $"Unexpected data count for {symbol}");

                        var collectionTimes = subDataFrame.index.get_level_values(0);
                        var symbols = subDataFrame.index.get_level_values(1);
                        var times = subDataFrame["time"];

                        for (var i = 0; i < collection.Data.Count; i++)
                        {
                            var datum = collection.Data[i];
                            Assert.AreEqual(collection.EndTime, collectionTimes[i].AsManagedObject(typeof(DateTime)));
                            Assert.AreEqual(datum.Symbol, symbols[i].AsManagedObject(typeof(Symbol)));
                            Assert.AreEqual(datum.EndTime, times[i].AsManagedObject(typeof(DateTime)));
                        }
                    }
                });

                AssertFlattenBaseDataCollectionDataFrameTimes(data, dataFrame);
            }
        }

        private static void AssertFlattenBaseDataCollectionDataFrameTimes(EnumerableData[] data, dynamic dataFrame)
        {
            // For base data collections, the end time of each data point is added as a column
            // And the time in the index is the collection's time
            var columnNames = new List<string>();
            foreach (var column in dataFrame.columns)
            {
                columnNames.Add(column.__str__().AsManagedObject(typeof(string)));
            }
            CollectionAssert.Contains(columnNames, "time");

            var times = dataFrame.time.AsManagedObject(typeof(DateTime[]));
            CollectionAssert.AreEqual(data.SelectMany(collection => collection.Select(x => x.EndTime)), times);
        }

        [Test]
        public void HandlesEnumerableWithMultipleSymbols()
        {
            var converter = new PandasConverter();
            var data = new List<BaseData>
            {
                new TradeBar(new DateTime(2020, 1, 2), Symbols.IBM, 101m, 102m, 100m, 101m, 10m),
                new TradeBar(new DateTime(2020, 1, 3), Symbols.IBM, 101m, 102m, 100m, 101m, 20m),
                new TradeBar(new DateTime(2020, 1, 2), Symbols.SPY_C_192_Feb19_2016, 51m, 52m, 50m, 51m, 100m),
                new TradeBar(new DateTime(2020, 1, 3), Symbols.SPY_C_192_Feb19_2016, 51m, 52m, 50m, 51m, 200m),
            };

            dynamic dataFrame = converter.GetDataFrame(data, forceMultiValueSymbol: true);

            using (Py.GIL())
            {
                Assert.Multiple(() =>
                {
                    foreach (var symbol in data.Select(x => x.Symbol).Distinct())
                    {
                        Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)), $"Unexpected empty sub dataframe for {symbol}");

                        var subDataFrame = dataFrame.loc[symbol];
                        Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                        var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                        Assert.AreEqual(2, count, $"Unexpected rows count for {symbol} sub dataframe");

                        var dataCount = subDataFrame.values.__len__().AsManagedObject(typeof(int));
                        Assert.AreEqual(2, dataCount, $"Unexpected rows count for {symbol} sub dataframe");
                    }
                });
            }
        }

        [Test]
        public void HandlesEmptyEnumerable()
        {
            var converter = new PandasConverter();
            var rawBars = Enumerable.Empty<TradeBar>().ToArray();

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsTrue(dataFrame.empty.AsManagedObject(typeof(bool)));
            }

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            var history = GetHistory(Symbols.SPY, Resolution.Minute, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsTrue(dataFrame.empty.AsManagedObject(typeof(bool)));
            }
        }

        [Test]
        public void HandlesTradeBars()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.SPY;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new TradeBar(DateTime.UtcNow.AddMinutes(i), symbol, i + 101m, i + 102m, i + 100m, i + 101m, 0m))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var close = subDataFrame.loc[index].close.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Close, close);
                }
            }

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            var history = GetHistory(symbol, Resolution.Minute, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var close = subDataFrame.loc[index].close.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Close, close);
                }
            }
        }

        [Test]
        public void HandlesQuoteBars()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.EURUSD;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new QuoteBar(DateTime.UtcNow.AddMinutes(i), symbol, new Bar(i + 1.01m, i + 1.02m, i + 1.00m, i + 1.01m), 0m, new Bar(i + 1.01m, i + 1.02m, i + 1.00m, i + 1.01m), 0m))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var close = subDataFrame.loc[index].close.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Close, close);
                }
            }

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            var history = GetHistory(symbol, Resolution.Minute, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var close = subDataFrame.loc[index].askclose.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Ask.Close, close);
                }
            }
        }

        [Test]
        public void HandlesNullableValues()
        {
            var converter = new PandasConverter();
            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new NullableValueData
                {
                    Symbol = Symbols.AAPL,
                    EndTime = new DateTime(2020, 1, 1),
                    NullableInt = null,
                    NullableTime = null,
                    NullableColumn = null
                })
                .ToArray();

            rawBars[0].NullableInt = 0;
            rawBars[1].NullableTime = new DateTime(2020, 1, 2);

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.AreEqual(2, dataFrame.columns.__len__().AsManagedObject(typeof(int)));
                var count = dataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(10, count);
            }

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            var history = GetHistory(Symbols.AAPL, Resolution.Daily, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.AreEqual(2, dataFrame.columns.__len__().AsManagedObject(typeof(int)));
                var count = dataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(10, count);
            }
        }

        /// <summary>
        /// Specific issues for symbol LOW, reference GH issue #4886
        /// </summary>
        [Test]
        public void HandlesOddTickers()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.LOW;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new TradeBar(DateTime.UtcNow.AddMinutes(i), symbol, i + 101m, i + 102m, i + 100m, i + 101m, 0m))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            var history = GetHistory(symbol, Resolution.Minute, rawBars);
            dynamic dataFrame = converter.GetDataFrame(history);

            // Add LOW to our symbol cache
            SymbolCache.Set("LOW", Symbols.LOW);

            using (Py.GIL())
            {

                dynamic test = PyModule.FromString("testModule",
    $@"
def Test(dataFrame):
    data = dataFrame.loc['LOW']
    if data.empty:
        raise Exception('LOW history data is empty')
    if data.__len__() != 10:
        raise Exception('Expected 10 data points')
    lowData = data.low
    if lowData.empty:
        raise Exception('LOW history low data is empty')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(dataFrame));

            }
        }

        [Test]
        public void BreakingOddTickers()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.LOW;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new TradeBar(DateTime.UtcNow.AddMinutes(i), symbol, i + 101m, i + 102m, i + 100m, i + 101m, 0m))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<TradeBar>
            var history = GetHistory(symbol, Resolution.Minute, rawBars);
            dynamic dataFrame = converter.GetDataFrame(history);

            // Add LOW to our symbol cache
            SymbolCache.Set("LOW", Symbols.LOW);

            using (Py.GIL())
            {

                var Test = PyModule.FromString("testModule",
    $@"
def Test1(dataFrame):
    # Should not throw, access all LOW ticker data
    data = dataFrame.loc['LOW']
def Test2(dataFrame):
    # Bad accessor, expected to throw
    data = dataFrame.LOW
def Test3(dataFrame):
    # Should not throw, access all LOW ticker data
    data = dataFrame.loc['low']
def Test4(dataFrame):
    # Should not throw, access data column low for all tickers
    data = dataFrame.low
");

                dynamic test1 = Test.GetAttr("Test1");
                dynamic test2 = Test.GetAttr("Test2");
                dynamic test3 = Test.GetAttr("Test3");
                dynamic test4 = Test.GetAttr("Test4");

                Assert.DoesNotThrow(() => test1(dataFrame));
                Assert.Throws<PythonException>(() => test2(dataFrame));
                Assert.DoesNotThrow(() => test3(dataFrame));
                Assert.DoesNotThrow(() => test4(dataFrame));
            }
        }

        [TestCase(true)]
        [TestCase(false)]
        public void BackwardsCompatibilityDataFrame_Subset(bool cache)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.droplevel('symbol')
    data = data[['lastprice', 'quantity']][:-1]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_add_prefix(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.add_prefix('price_')['price_lastprice'].unstack(level=0)
    data = data.iloc[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_add_suffix(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.add_suffix('_tick')['lastprice_tick'].unstack(level=0)
    data = data.iloc[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_align(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    data = dataFrame.lastprice.unstack(level=0)
    other = other.lastprice.unstack(level=0)
    data, other = data.align(other, axis=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_apply(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).apply(np.sqrt)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_applymap(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).applymap(lambda x: x*2)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_asfreq(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).asfreq(freq='30S')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_asof(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, symbol):
    idx = pd.DatetimeIndex(['2018-02-27 09:03:30'])
    data = dataFrame.lastprice.unstack(level=0).asof(idx)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_assign(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.assign(tmp=lambda x: x.lastprice * 1.1)['tmp'].unstack(level=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_astype(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).astype('float16')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_at(string index, bool cache = false)
        {
            // Syntax like ({index},) deprecated in newer version of pandas
            // Same result achievable by .loc[{index}]['lastprice']
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.at[({index},), 'lastprice']").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_at_time(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).at_time('04:00')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_axes(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    axes = dataFrame.axes[0]
    if {index} not in axes.levels[0]:
        raise ValueError('SPY was not found')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_between_time(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).between_time('02:00', '06:00')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_columns(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    columns = dataFrame.lastprice.unstack(level=0).columns
    if {index} not in columns:
        raise ValueError('SPY was not found')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_combine(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, other, symbol):
    dataFrame = dataFrame.lastprice.unstack(level=0)
    other = other.lastprice.unstack(level=0)
    data = dataFrame.combine(other, np.minimum)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_concat(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, dataFrame2, symbol):
    newDataFrame = pd.concat([dataFrame, dataFrame2])
    data = newDataFrame['lastprice'].unstack(level=0).iloc[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_corrwith(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, other, symbol):
    other = other.lastprice.unstack(level=0)
    data = dataFrame.lastprice.unstack(level=0).corrwith(other)
    data = data.loc[{index}]
    if not np.isnan(data):
        raise Exception('Data should be NaN')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_drop(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.drop(columns=['exchange']).lastprice.unstack(level=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_droplevel(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.droplevel('time').lastprice
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_dtypes(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).dtypes
    data = data.loc[{index}]
    if data != np.float64:
        raise Exception('Data type is ' + str(data))").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_eval(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.eval('tmp=lastprice * 1.1')['tmp'].unstack(level=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_equals(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    if not dataFrame.equals(other):
        raise Exception('Dataframe are not equal')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_ewm(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0)
    data = data.ewm(com=0.5).mean()
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_expanding(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0)
    data = data.expanding(2).sum()
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_explode(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.explode('lastprice').lastprice.unstack(level=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_filter(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.filter(items=['lastprice']).lastprice.unstack(level=0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_ftypes(string index, bool cache = false)
        {
            // ftypes deprecated in newer pandas; use dtypes instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0).ftypes
    data = data.loc[{index}]
    if str(data) != 'float64:dense':
        raise Exception('Data type is ' + str(data))").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_get_OnProperty(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.get({index})
    if data.empty:
        raise Exception('Data is empty')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_getitem(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame['lastprice'][{index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_get_value_index(string index, bool cache = false)
        {
            // get_value deprecated in new Pandas; Syntax also deprecated; Use .at[] or .iat[] instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.get_value(({index},), 'lastprice')
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_get_value_column(string index, bool cache = false)
        {
            // get_value deprecated in new Pandas; use at[] or iat[] instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0)
    idx = data.index[0]
    data = data.get_value(idx, {index})
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_groupby(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(df, other, symbol):
    df = pd.concat([df, other])
    df = df.groupby(level=0).mean(numeric_only=True)
    data = df.lastprice.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_iloc(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame['lastprice'].unstack(level=0).iloc[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_insert(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(level=0)
    data.insert(0, 'nine', 999)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");
                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_index_levels_contains(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    if {index} not in dataFrame.index.levels[0]:
        raise ValueError('SPY was not found')").GetAttr("Test");
                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }


        [TestCase("items", "'SPY'", true)]
        [TestCase("items", "symbol")]
        [TestCase("items", "str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_items(string method, string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    for index, data in dataFrame.{method}():
        pass
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_iterrows(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    for index, data in dataFrame.lastprice.unstack(level=0).iterrows():
        pass
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_ix(string index, bool cache = false)
        {
            // ix deprecated in newer pandas; use loc and iloc instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame['lastprice'].unstack(level=0).ix[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_join(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, dataFrame2, symbol):
    newDataFrame = dataFrame.join(dataFrame2, lsuffix='_')
    data = newDataFrame['lastprice_'].unstack(level=0).iloc[-1][{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_loc(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.loc[{index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("['SPY','AAPL']", true)]
        [TestCase("symbols")]
        [TestCase("[str(symbols[0].ID), str(symbols[1].ID)]")]
        [TestCase("('SPY','AAPL')", true)]
        [TestCase("(str(symbols[0].ID), str(symbols[1].ID))")]
        public void BackwardsCompatibilityDataFrame_loc_list(string index, bool cache = false)
        {
            if (cache)
            {
                SymbolCache.Set("SPY", Symbols.SPY);
                SymbolCache.Set("AAPL", Symbols.AAPL);
            }

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbols):
    data = dataFrame.lastprice.unstack(0)
    data = data.loc[:, {index}]").GetAttr("Test");

                var symbols = new List<Symbol> { Symbols.SPY, Symbols.AAPL };
                Assert.DoesNotThrow(() => test(GetTestDataFrame(symbols), symbols));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_loc_after_xs(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    time = dataFrame.index.get_level_values('time')[0]
    dataFrame = dataFrame.xs(time, level='time')
    data = dataFrame.loc[{index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_loc_OnProperty(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.loc[{index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_loc_SubDataFrame(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.loc[{index}].loc['2013-10-07 04:00:00']").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("*symbols", true)]
        [TestCase("*[str(symbol.ID) for symbol in symbols]", true)]
        [TestCase("'AAPL', 'GOOG'", true)]
        [TestCase("*symbols", false)]
        [TestCase("*[str(symbol.ID) for symbol in symbols]", false)]
        [TestCase("'AAPL', 'GOOG'", false)]
        public void BackwardsCompatibilityDataFrame_loc_slicers(string index, bool cache)
        {
            if (cache)
            {
                SymbolCache.Set("SPY", Symbols.SPY);
                SymbolCache.Set("AAPL", Symbols.AAPL);
                SymbolCache.Set("GOOG", Symbols.GOOG);
            }

            using (Py.GIL())
            {
                dynamic testModule = PyModule.FromString("testModule",
                    $@"
def sortDataFrameIndex(dataFrame):
    dataFrame.sort_index(ascending=True, inplace=True)

def indexDataFrameBySymbols(dataFrame, symbols):
    # MultiIndex slicing requires the index to be lexsorted
    sortDataFrameIndex(dataFrame)
    return dataFrame.loc[(slice({index}), slice(None)), :]

def indexDataFrameBySymbol(dataFrame, symbol):
    sortDataFrameIndex(dataFrame)
    return dataFrame.loc[(symbol, slice(None)), :]
");
                dynamic indexDataFrameBySymbols = testModule.GetAttr("indexDataFrameBySymbols");
                dynamic indexDataFrameBySymbol = testModule.GetAttr("indexDataFrameBySymbol");

                var symbols = new List<Symbol> { Symbols.SPY, Symbols.AAPL, Symbols.GOOG };
                var dataFrame = GetTestDataFrame(symbols);

                var looupkSymbols = new List<Symbol> { Symbols.AAPL, Symbols.GOOG };
                dynamic indexedDataFrame = null;
                Assert.DoesNotThrow(() => indexedDataFrame = indexDataFrameBySymbols(dataFrame, looupkSymbols));
                Assert.IsNotNull(indexedDataFrame);

                dynamic aaplDataFrame = null;
                Assert.DoesNotThrow(() => aaplDataFrame = indexDataFrameBySymbol(indexedDataFrame, Symbols.AAPL));
                Assert.IsNotNull(aaplDataFrame);

                dynamic googDataFrame = null;
                Assert.DoesNotThrow(() => googDataFrame = indexDataFrameBySymbol(indexedDataFrame, Symbols.GOOG));
                Assert.IsNotNull(googDataFrame);

                // SPY entries should not be present in the indexed data frame since it was not part of the lookup symbols
                dynamic spyDataFrame = null;
                Assert.Throws<PythonException>(() => spyDataFrame = indexDataFrameBySymbol(indexedDataFrame, Symbols.SPY));
                Assert.IsNull(spyDataFrame);
            }
        }

        [TestCase("2013-10-07 04:00:00", true)]
        [TestCase("2013-10-07 04:00:00", false)]
        public void BackwardsCompatibilityDataFrame_loc_slicers_none(string index, bool cache)
        {
            if (cache)
            {
                SymbolCache.Set("SPY", Symbols.SPY);
                SymbolCache.Set("AAPL", Symbols.AAPL);
            }

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame):
    return dataFrame.loc[(slice(None), '{index}'), 'lastprice']").GetAttr("Test");

                var symbols = new List<Symbol> { Symbols.SPY, Symbols.AAPL };
                Assert.DoesNotThrow(() => test(GetTestDataFrame(symbols)));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_mask(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(0)
    data = data.mask(data > 0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_pivot_table(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, symbol):
    df = dataFrame.reset_index()
    table = pd.pivot_table(df, index=['symbol', 'time'], aggfunc='first')
    data = table.lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_merge(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, dataFrame2, symbol):
    newDataFrame = dataFrame.merge(dataFrame2, on='symbol', how='outer')
    data = newDataFrame.loc[{index}]
    if len(data) is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("nlargest", "'SPY'", true)]
        [TestCase("nlargest", "symbol")]
        [TestCase("nlargest", "str(symbol.ID)")]
        [TestCase("nsmallest", "'SPY'", true)]
        [TestCase("nsmallest", "symbol")]
        [TestCase("nsmallest", "str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_nlarg_smallest(string method, string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.{method}(3, 'lastprice')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_pipe(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, other, symbol):
    def mean_by_group(dataframe, level):
        return dataframe.groupby(level=level).mean(numeric_only=True)

    df = pd.concat([dataFrame, other])
    data = df.pipe(mean_by_group, level=0)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_pop(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.pop('lastprice')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_query(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.query('lastprice > 100').lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_query_index(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    time = '2011-02-01'
    data = dataFrame.lastprice.unstack(0).query('index > @time')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_reindex_like(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    data = other.reindex_like(dataFrame)
    data = data.lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_rename(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            var rename = "columns={'lastprice': 'price', 'B': 'c'}";

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.rename({rename})['price'].unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_reorder_levels(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.reorder_levels(['time', 'symbol']).lastprice.unstack(1)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_resample(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(0)
    data = data.resample('2S').sum()
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_reset_index(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(0).reset_index('time')
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_rolling(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.rolling(2).sum(numeric_only=True)
    data = data.lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_select_dtypes(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.select_dtypes(exclude=['int']).lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_set_value(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            // set_value deprecated in newer pandas; use .at[] or .iat instead
            if (_newerPandas)
            {
                return;
            }

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    idx = dataFrame.first_valid_index()
    data = dataFrame.set_value(idx, 'lastprice', 100).lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 3), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_shift(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.shift().lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_sort_values(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.sort_values(by=['lastprice']).lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_swapaxes(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(0).swapaxes('index', 'columns')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_swaplevel(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.swaplevel().lastprice.unstack(1)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_take(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.take([0, 3]).lastprice.unstack(0)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_transform(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.unstack(0).transform(np.sqrt)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_T(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.T.iloc[0]
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_transpose(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.transpose().iloc[0]
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_series_shift(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
from datetime import timedelta as d
def Test(dataFrame, symbol):
    series = dataFrame.droplevel(0)
    data = series.shift(freq=d(1))").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_tz_localize_convert(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.tz_localize('UTC', level=1)
    data = data.tz_convert('America/New_York', level=1)
    data = data.lastprice.unstack(0)[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_unstack_lastprice(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.unstack(level=0).lastprice[{index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_unstack_loc_loc(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    df2 = dataFrame.unstack(level=0)
    df3 = df2.loc[:,'lastprice']
    data = df3.loc[:, {index}]").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_unstack_get(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    df2 = dataFrame.lastprice.unstack(level=0)
    data = df2.get({index})
    if data.empty:
        raise Exception('Data is empty')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_update(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            var other = "{'lastprice': ['d', 'e', 'f']}";

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, symbol):
    other = pd.DataFrame({other}, index=dataFrame.index)
    dataFrame.update(other)
    data = dataFrame.lastprice.unstack(0)[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 3), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_where(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    dataFrame = dataFrame.lastprice.unstack(0)
    data = dataFrame.where(dataFrame > 167)
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 3), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilityDataFrame_xs(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.xs({index})").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol)")]
        public void BackwardsCompatibilitySeries__str__(string symbol, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);
            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
from datetime import datetime as dt
def Test(dataFrame, symbol):
    close = dataFrame.lastprice.unstack(0)
    to_append = pd.Series([100], name={symbol}, index=pd.Index([dt.now()], name='time'))
    result = pd.concat([close, to_append], ignore_index=True)
    return str([result[x] for x in [symbol]])").GetAttr("Test");
                var result = "Remapper";
                Assert.DoesNotThrow(() => result = test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
                // result should not contain "Remapper" string because the test
                // returns the string representation of a Series object
                Assert.False(result.Contains("Remapper"));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol)")]
        public void BackwardsCompatibilitySeries__repr__(string symbol, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);
            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
from datetime import datetime as dt
def Test(dataFrame, symbol):
    close = dataFrame.lastprice.unstack(0)
    to_append = pd.Series([100], name={symbol}, index=pd.Index([dt.now()], name='time'))
    result = pd.concat([close, to_append], ignore_index=True)
    return repr([result[x] for x in [symbol]])").GetAttr("Test");
                var result = "Remapper";
                Assert.DoesNotThrow(() => result = test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
                // result should not contain "Remapper" string because the test
                // returns the string representation of a Series object
                Assert.False(result.Contains("Remapper"));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_align(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    data = dataFrame.lastprice
    other = other.lastprice
    data, other = data.align(other, axis=0)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_apply(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.apply(np.sqrt)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_asfreq(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice.droplevel(0)
    data = series.asfreq(freq='30S')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_asof(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, symbol):
    idx = pd.DatetimeIndex(['2018-02-27 09:03:30'])
    series = dataFrame.lastprice.droplevel(0)
    data = series.asof(idx)").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_astype(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    data = dataFrame.lastprice.astype('float16')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_at_time(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice.droplevel(0)
    data = series.at_time('04:00')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_between(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.between(100, 200)").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_between_time(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice.droplevel(0)
    data = series.between_time('02:00', '06:00')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_combine(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, other, symbol):
    series = dataFrame.lastprice
    other = other.lastprice
    data = series.combine(other, np.minimum)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_drop(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    dt = series.first_valid_index()[1]
    data = series.drop(dt, level=1)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 2), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_droplevel(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.droplevel('time')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_equals(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    series = dataFrame.lastprice
    other = other.lastprice
    if not series.equals(other):
        raise Exception('Dataframe are not equal')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_ewm(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.ewm(com=0.5).mean()
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_expanding(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.expanding(2).sum()
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_filter(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.filter(like='04:00')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_first(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice.droplevel(0)
    data = series.first('2S')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_get(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.get({index})
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_get_value(string index, bool cache = false)
        {
            // get_value deprecated in new Pandas; Use .at[] or .iat[] instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    dt = series.first_valid_index()[1]
    data = series.get_value(({index}, dt))
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_groupby(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(df, other, symbol):
    series = pd.concat([df, other]).lastprice
    data = series.groupby(level=0).mean()
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_last(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice.droplevel(0)
    data = series.last('2S')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_map(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            var map = "map({167: 100})";

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.{map}
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_mask(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.mask(series > 0)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_pipe(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, other, symbol):
    def mean_by_group(dataframe, level):
        return dataframe.groupby(level=level).mean()

    df = pd.concat([dataFrame, other]).lastprice
    data = df.pipe(mean_by_group, level=0)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_reindex_like(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, other, symbol):
    series = dataFrame.lastprice
    other = other.lastprice
    data = other.reindex_like(series)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_rename(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.rename('price')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_repeat(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.repeat(3)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_reorder_levels(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.reorder_levels(['time', 'symbol'])
    data = data.unstack(1)[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_resample(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.resample('2S', level=1).mean()").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_reset_index(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.reset_index('time')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_rolling(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.rolling(2).sum()
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }


        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_set_value(string index, bool cache = false)
        {
            // set_value deprecated in newer pandas; Use .at[] or .iat[] instead
            if (_newerPandas)
            {
                return;
            }

            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    idx = series.first_valid_index()
    data = series.set_value(idx, 100)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_shift(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.shift()
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_swapaxes(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.swapaxes('index', 'index')
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_swaplevel(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.swaplevel()
    data = data.unstack(0).loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_take(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.take([0, 3])
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 10), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_transform(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import numpy as np
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.transform([np.sqrt, np.exp])
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }


        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_T(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.T
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_transpose(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.transpose()
    data = data[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_tz_localize_convert(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.tz_localize('UTC', level=1)
    data = data.tz_convert('America/New_York', level=1)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_update(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
import pandas as pd
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    other = pd.Series(['d', 'e', 'f'], index=series.index)
    series.update(other)
    data = series.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 3), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_where(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.where(series > 167)
    data = data.loc[{index}]
    if data is 0:
        raise Exception('Data is zero')").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY, 3), Symbols.SPY));
            }
        }

        [TestCase("'SPY'", true)]
        [TestCase("symbol")]
        [TestCase("str(symbol.ID)")]
        public void BackwardsCompatibilitySeries_xs(string index, bool cache = false)
        {
            if (cache) SymbolCache.Set("SPY", Symbols.SPY);

            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
def Test(dataFrame, symbol):
    series = dataFrame.lastprice
    data = series.xs({index})").GetAttr("Test");

                Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }


        [Test]
        public void NotBackwardsCompatibilityDataFrame_index_levels_contains_ticker_notInCache()
        {
            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    @"
def Test(dataFrame, symbol):
    if 'SPY' not in dataFrame.index.levels[0]:
        raise ValueError('SPY was not found')").GetAttr("Test");
                Assert.Throws<PythonException>(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
            }
        }

        [Test]
        public void HandlesTradeTicks()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.SPY;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new Tick(symbol, $"1440{i:D2}00,167{i:D2}00,1{i:D2},T,T,0", new DateTime(2013, 10, 7)))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                Assert.IsTrue(subDataFrame.get("askprice") == null);
                Assert.IsTrue(subDataFrame.get("exchange") != null);

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].lastprice.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].LastPrice, value);
                }
            }

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            var history = GetHistory(symbol, Resolution.Tick, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                Assert.IsTrue(subDataFrame.get("askprice") == null);
                Assert.IsTrue(subDataFrame.get("exchange") != null);

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].lastprice.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].LastPrice, value);
                }
            }
        }

        [Test]
        public void HandlesQuoteTicks()
        {
            var converter = new PandasConverter();
            var symbol = Symbols.EURUSD;

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i => new Tick(DateTime.UtcNow.AddMilliseconds(100 * i), symbol, 0.99m, 1.01m))
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                Assert.IsTrue(subDataFrame.get("askprice") != null);
                Assert.IsTrue(subDataFrame.get("exchange") == null);

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].lastprice.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].LastPrice, value);
                }
            }

            // GetDataFrame with argument of type IEnumerable<QuoteBar>
            var history = GetHistory(symbol, Resolution.Tick, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                Assert.IsTrue(subDataFrame.get("askprice") != null);
                Assert.IsTrue(subDataFrame.get("exchange") == null);

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].askprice.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].AskPrice, value);
                }
            }
        }


        private static Resolution[] ResolutionCases = { Resolution.Tick, Resolution.Minute, Resolution.Second };
        private static Symbol[] SymbolCases = { Symbols.Fut_SPY_Feb19_2016, Symbols.Fut_SPY_Mar19_2016, Symbols.SPY_C_192_Feb19_2016, Symbols.SPY_P_192_Feb19_2016 };

        [Test]
        public void HandlesOpenInterestTicks([ValueSource(nameof(ResolutionCases))] Resolution resolution, [ValueSource(nameof(SymbolCases))] Symbol symbol)
        {
            // Arrange
            var converter = new PandasConverter();
            var tickType = TickType.OpenInterest;
            var dataType = LeanData.GetDataType(resolution, tickType);
            var subcriptionDataConfig = new SubscriptionDataConfig(dataType, symbol, resolution,
                                                                   TimeZones.Chicago, TimeZones.Chicago,
                                                                   tickType: tickType, fillForward: false,
                                                                   extendedHours: true, isInternalFeed: true);
            var openinterest = new List<OpenInterest>();
            for (int i = 0; i < 10; i++)
            {
                var line = $"{1000 * i},{11 * i}";
                var openInterestTicks = new OpenInterest(subcriptionDataConfig, symbol, line, new DateTime(2017, 10, 10));
                openinterest.Add(openInterestTicks);
            }

            // Act
            dynamic dataFrame = converter.GetDataFrame(openinterest);

            //Assert
            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                Assert.IsTrue(subDataFrame.get("openinterest") != null);

                var count = subDataFrame.shape[0].AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].openinterest.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(openinterest[i].Value, value);
                }
            }

        }

        [Test]
        [TestCase(typeof(CustomData), "yyyy-MM-dd")]
        [TestCase(typeof(FxcmVolume), "yyyyMMdd HH:mm")]
        public void HandlesCustomDataBars(Type type, string format)
        {
            var converter = new PandasConverter();
            var symbol = Symbols.LTCUSD;

            var config = GetSubscriptionDataConfig<BaseData>(symbol, Resolution.Daily);
            var custom = Activator.CreateInstance(type) as BaseData;
            if (type == typeof(CustomData)) custom.Reader(config, "date,open,high,low,close,transactions", DateTime.UtcNow, false);

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i =>
                {
                    var line = $"{DateTime.UtcNow.AddDays(i).ToStringInvariant(format)},{i + 101},{i + 102},{i + 100},{i + 101},{i + 101}";
                    return custom.Reader(config, line, DateTime.UtcNow.AddDays(i), false);
                })
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<BaseData>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);

                    var transactions = subDataFrame.loc[index].transactions.AsManagedObject(typeof(decimal));
                    var expected = (rawBars[i] as DynamicData)?.GetProperty("transactions");
                    expected = expected ?? type.GetProperty("Transactions")?.GetValue(rawBars[i]);
                    Assert.AreEqual(expected, transactions);
                }
            }

            // GetDataFrame with argument of type IEnumerable<BaseData>
            var history = GetHistory(symbol, Resolution.Daily, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(10, count);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);
                    var transactions = subDataFrame.loc[index].transactions.AsManagedObject(typeof(decimal));
                    var expected = (rawBars[i] as DynamicData)?.GetProperty("transactions");
                    expected = expected ?? type.GetProperty("Transactions")?.GetValue(rawBars[i]);
                    Assert.AreEqual(expected, transactions);
                }
            }
        }

        [Test]
        public void HandlesCustomDataWithValueColumn()
        {
            // Reproduce issue #5596
            // Value column data is duplicated in series

            var converter = new PandasConverter();
            var symbol = Symbols.LTCUSD;

            var config = GetSubscriptionDataConfig<BaseData>(symbol, Resolution.Daily);
            var custom = Activator.CreateInstance(typeof(CustomData)) as BaseData;
            custom.Reader(config, "Date,Value", DateTime.UtcNow, false);

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i =>
                {
                    var line = $"{DateTime.UtcNow.AddDays(i).ToStringInvariant("yyyy-MM-dd")},{i + 40000}";
                    return custom.Reader(config, line, DateTime.UtcNow.AddDays(i), false);
                })
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<BaseData>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);
                }
            }

            // GetDataFrame with argument of type IEnumerable<Slices>
            var history = GetHistory(symbol, Resolution.Daily, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(10, count);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);
                }
            }
        }

        [Test]
        [TestCase(typeof(SubTradeBar), "SubProperty")]
        [TestCase(typeof(SubSubTradeBar), "SubSubProperty")]
        public void HandlesCustomDataBarsInheritsFromTradeBar(Type type, string propertyName)
        {
            var converter = new PandasConverter();
            var symbol = Symbols.LTCUSD;

            var config = GetSubscriptionDataConfig<UnlinkedDataTradeBar>(symbol, Resolution.Daily);
            dynamic custom = Activator.CreateInstance(type);

            var rawBars = Enumerable
                .Range(0, 10)
                .Select(i =>
                {
                    var line = $"{DateTime.UtcNow.AddDays(i).ToStringInvariant("yyyyMMdd HH:mm")},{i + 101},{i + 102},{i + 100},{i + 101},{i + 101}";
                    return custom.Reader(config, line, DateTime.UtcNow.AddDays(i), false) as BaseData;
                })
                .ToArray();

            // GetDataFrame with argument of type IEnumerable<BaseData>
            dynamic dataFrame = converter.GetDataFrame(rawBars);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(count, 10);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);

                    var transactions = subDataFrame.loc[index][propertyName.ToLowerInvariant()].AsManagedObject(typeof(decimal));
                    var expected = type.GetProperty(propertyName)?.GetValue(rawBars[i]);
                    Assert.AreEqual(expected, transactions);
                }
            }

            // GetDataFrame with argument of type IEnumerable<BaseData>
            var history = GetHistory(symbol, Resolution.Daily, rawBars);
            dataFrame = converter.GetDataFrame(history);

            using (Py.GIL())
            {
                Assert.IsFalse(dataFrame.empty.AsManagedObject(typeof(bool)));

                var subDataFrame = dataFrame.loc[symbol];
                Assert.IsFalse(subDataFrame.empty.AsManagedObject(typeof(bool)));

                var count = subDataFrame.__len__().AsManagedObject(typeof(int));
                Assert.AreEqual(10, count);

                for (var i = 0; i < count; i++)
                {
                    var index = subDataFrame.index[i];
                    var value = subDataFrame.loc[index].value.AsManagedObject(typeof(decimal));
                    Assert.AreEqual(rawBars[i].Value, value);
                }
            }
        }

        private static object[] SpotMarketCases => LeanDataReaderTests.SpotMarketCases;
        private static object[] OptionAndFuturesCases => LeanDataReaderTests.OptionAndFuturesCases;

        [Test, TestCaseSource(nameof(SpotMarketCases))]
        public void HandlesLeanDataReaderOutputForSpotMarkets(string securityType, string market, string resolution, string ticker, string fileName, int rowsInfile, double sumValue)
        {
            using (Py.GIL())
            {
                // Arrange
                var dataFolder = "../../../Data";
                var filepath = LeanDataReaderTests.GenerateFilepathForTesting(dataFolder, securityType, market, resolution, ticker, fileName);
                var leanDataReader = new LeanDataReader(filepath);
                var data = leanDataReader.Parse();
                var converter = new PandasConverter();
                // Act
                dynamic df = converter.GetDataFrame(data);
                // Assert
                Assert.AreEqual(rowsInfile, df.shape[0].AsManagedObject(typeof(int)));

                int columnsNumber = df.shape[1].AsManagedObject(typeof(int));
                if (columnsNumber == 3 || columnsNumber == 6)
                {
                    Assert.AreEqual(sumValue, df.get("lastprice").sum().AsManagedObject(typeof(double)), 1e-4);
                }
                else
                {
                    Assert.AreEqual(sumValue, df.get("close").sum().AsManagedObject(typeof(double)), 1e-4);
                }
            }
        }

        [Test, TestCaseSource(nameof(OptionAndFuturesCases))]
        public void HandlesLeanDataReaderOutputForOptionAndFutures(string composedFilePath, Symbol symbol, int rowsInfile, double sumValue)
        {
            using (Py.GIL())
            {
                // Arrange
                var leanDataReader = new LeanDataReader(composedFilePath);
                var data = leanDataReader.Parse();
                var converter = new PandasConverter();
                // Act
                dynamic df = converter.GetDataFrame(data);
                // Assert
                Assert.AreEqual(rowsInfile, df.shape[0].AsManagedObject(typeof(int)));

                int columnsNumber = df.shape[1].AsManagedObject(typeof(int));
                var lastPrice = df.get("lastprice");
                if (lastPrice != null)
                {
                    Assert.AreEqual(sumValue, lastPrice.sum().AsManagedObject(typeof(double)), 1e-4);
                }
                else if (columnsNumber == 1)
                {
                    Assert.AreEqual(sumValue, df.get("openinterest").sum().AsManagedObject(typeof(double)), 1e-4);
                }
                else
                {
                    Assert.AreEqual(sumValue, df.get("close").sum().AsManagedObject(typeof(double)), 1e-4);
                }
            }
        }

        [Test]
        public void AcceptsPythonDictAsIndicatorData()
        {
            using (Py.GIL())
            {
                dynamic test = PyModule.FromString("testModule",
                    $@"
from QuantConnect.Python import PandasConverter
from QuantConnect.Indicators import IndicatorDataPoint;
def Test():
    pdConverter = PandasConverter()
    return pdConverter.GetIndicatorDataFrame({{'ind': [IndicatorDataPoint()]}})").GetAttr("Test");

                Assert.DoesNotThrow(() => test());
            }
        }

        [Test]
        public void DoesntAcceptOtherPythonObjectRatherThanDict()
        {
            using (Py.GIL())
            {
                dynamic tests = PyModule.FromString("testModule",
                    $@"
from QuantConnect.Python import PandasConverter
from QuantConnect.Indicators import IndicatorDataPoint;
pdConverter = PandasConverter()
def Test1():
    pdConverter.GetIndicatorDataFrame([""ind"", IndicatorDataPoint()])
def Test2():
    pdConverter.GetIndicatorDataFrame(IndicatorDataPoint())
def Test3():
    pdConverter.GetIndicatorDataFrame({{'ind': IndicatorDataPoint()}})");

                dynamic test1 = tests.GetAttr("Test1");
                dynamic test2 = tests.GetAttr("Test2");
                dynamic test3 = tests.GetAttr("Test3");

                Assert.That(() => test1(), Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>());
                Assert.That(() => test2(), Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>());
                Assert.That(() => test3(), Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>());
            }
        }

        [Test]
        public void ReturnsTheCorrectDataInTheDataFrameFromPyDict()
        {
            using (Py.GIL())
            {
                dynamic tests = PyModule.FromString("testModule",
                    $@"
from datetime import datetime, timedelta
from QuantConnect.Python import PandasConverter
from QuantConnect.Indicators import IndicatorDataPoint;

def DataFrameContainsCorrectData():
    dateTime = datetime(2018, 1, 1)
    makePoint = lambda i: IndicatorDataPoint(dateTime + timedelta(minutes=i), i)
    indicator1DataPoints = [makePoint(i) for i in range(10)]
    indicator2DataPoints = [makePoint(i) for i in range(5)]
    indicator3DataPoints = []
    pdConverter = PandasConverter()
    dataFrame = pdConverter.GetIndicatorDataFrame({{
        'ind1': indicator1DataPoints,
        'ind2': indicator2DataPoints,
        'ind3': indicator3DataPoints
    }})

    hasData = lambda key, data: dataFrame[key].tolist()[:len(data)] == [point.Value for point in data]

    return (
        dataFrame.shape == (10, 3) and
        dataFrame.columns.tolist() == ['ind1', 'ind2', 'ind3'] and
        dataFrame.index.tolist() == [point.EndTime for point in indicator1DataPoints] and
        hasData('ind1', indicator1DataPoints) and
        hasData('ind2', indicator2DataPoints) and
        hasData('ind3', indicator3DataPoints)
    )

def DataFrameIsEmpty():
    indicatorDataPoints = []
    pdConverter = PandasConverter()
    dataFrame = pdConverter.GetIndicatorDataFrame({{'ind1': indicatorDataPoints}})

    return dataFrame.empty");

                dynamic dataFrameContainsCorrectData = tests.GetAttr("DataFrameContainsCorrectData");
                dynamic dataFrameIsEmpty = tests.GetAttr("DataFrameIsEmpty");

                Assert.IsTrue(dataFrameContainsCorrectData().As<bool>());
                Assert.IsTrue(dataFrameIsEmpty().As<bool>());
            }
        }

        [Test]
        public void ReturnsTheCorrectDataInTheDataFrameFromDictionary()
        {
            using (Py.GIL())
            {
                var dateTime = new DateTime(2018, 1, 1);
                Func<int, IndicatorDataPoint> makePoint = i => new IndicatorDataPoint(dateTime.AddMinutes(i), i);
                var indicator1DataPoints = Enumerable.Range(0, 10).Select(i => makePoint(i)).ToList();
                var indicator2DataPoints = Enumerable.Range(0, 5).Select(i => makePoint(i)).ToList();
                var indicator3DataPoints = new List<IndicatorDataPoint>();

                var pdConverter = new PandasConverter();
                var dataFrame = pdConverter.GetIndicatorDataFrame(new Dictionary<string, List<IndicatorDataPoint>>
                {
                    {"ind1", indicator1DataPoints},
                    {"ind2", indicator2DataPoints},
                    {"ind3", indicator3DataPoints}
                });

                Func<string, List<double>> getIndicatorData = key => dataFrame.GetItem(key).GetAttr("tolist").Invoke().As<List<double>>();
                CollectionAssert.AreEqual(new int[] { 10, 3 }, dataFrame.GetAttr("shape").As<int[]>());
                CollectionAssert.AreEqual(new string[] { "ind1", "ind2", "ind3" }, dataFrame.GetAttr("columns").As<string[]>());
                CollectionAssert.AreEqual(indicator1DataPoints.Select(point => point.EndTime).ToList(), dataFrame.GetAttr("index").GetAttr("tolist").Invoke().As<List<DateTime>>());
                CollectionAssert.AreEqual(indicator1DataPoints.Select(point => point.Value), getIndicatorData("ind1").GetRange(0, indicator1DataPoints.Count));
                CollectionAssert.AreEqual(indicator2DataPoints.Select(point => point.Value), getIndicatorData("ind2").GetRange(0, indicator2DataPoints.Count));
                CollectionAssert.AreEqual(indicator3DataPoints.Select(point => point.Value), getIndicatorData("ind3").GetRange(0, indicator3DataPoints.Count));
            }
        }

        [TestCaseSource(nameof(GetHistoryWithDuplicateTimes))]
        public void HandlesSlicesWithDuplicateTimeStamps(Symbol symbol, IEnumerable<Slice> data, string expectedDataFrameString)
        {
            var converter = new PandasConverter();
            dynamic dataFrame = null;
            Assert.DoesNotThrow(() => dataFrame = converter.GetDataFrame(data));

            using (Py.GIL())
            {
                Assert.AreEqual(expectedDataFrameString.Replace("\n", string.Empty).Replace("\r", string.Empty),
                    dataFrame.to_string().As<string>().Replace("\n", string.Empty).Replace("\r", string.Empty));
            }
        }

        [Test]
        public void RunDataFrameFromTickHistoryRegressionAlgorithm()
        {
            var parameter = new RegressionTests.AlgorithmStatisticsTestParameters(
                "PandasDataFrameFromMultipleTickTypeTickHistoryRegressionAlgorithm",
                new Dictionary<string, string> {
                    {PerformanceMetrics.TotalOrders, "0"},
                    {"Average Win", "0%"},
                    {"Average Loss", "0%"},
                    {"Compounding Annual Return", "0%"},
                    {"Drawdown", "0%"},
                    {"Expectancy", "0"},
                    {"Net Profit", "0%"},
                    {"Sharpe Ratio", "0"},
                    {"Probabilistic Sharpe Ratio", "0%"},
                    {"Loss Rate", "0%"},
                    {"Win Rate", "0%"},
                    {"Profit-Loss Ratio", "0"},
                    {"Alpha", "0"},
                    {"Beta", "0"},
                    {"Annual Standard Deviation", "0"},
                    {"Annual Variance", "0"},
                    {"Information Ratio", "0"},
                    {"Tracking Error", "0"},
                    {"Treynor Ratio", "0"},
                    {"Total Fees", "$0.00"}
                },
                Language.Python,
                AlgorithmStatus.Completed);

            AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
                parameter.Statistics,
                parameter.Language,
                parameter.ExpectedFinalStatus);
        }

        [Test]
        public void ConcatenatesDataFrames()
        {
            using (Py.GIL())
            {
                var test = PyModule.FromString("ConcatenatesDataFrames",
                    @"
import pandas as pd

index1 = pd.Index(['X', 'Y'], name=""Class"")
df1 = pd.DataFrame([[1, 2], [3, 4]], index=index1, columns=[""A"", ""B""])

index2 = pd.Index(['L', 'M'], name=""Class"")
df2 = pd.DataFrame([[5, 6], [7, 8]], index=index2, columns=[""A"", ""B""])

index3 = pd.Index(['R', 'S'], name=""Class"")
df3 = pd.DataFrame([[9, 10], [11, 12]], index=index3, columns=[""A"", ""B""])

concatenated = pd.concat([df1, df2, df3])
");

                using var df1 = test.GetAttr("df1");
                using var df2 = test.GetAttr("df2");
                using var df3 = test.GetAttr("df3");
                using var expected = test.GetAttr("concatenated");

                using var concatenated = PandasConverter.ConcatDataFrames(new[] { df1, df2, df3 }, sort: false, dropna: false);

                Assert.AreEqual(expected.GetAttr("to_string").Invoke().GetAndDispose<string>(),
                    concatenated.GetAttr("to_string").Invoke().GetAndDispose<string>());
            }
        }

        [Test]
        public void ConcatenatesDataFramesWithAddedIndexLevel()
        {
            using (Py.GIL())
            {
                var test = PyModule.FromString("ConcatenatesDataFramesWithAddedIndexLevel",
                    @"
import pandas as pd

index1 = pd.Index(['X', 'Y'], name=""Class"")
df1 = pd.DataFrame([[1, 2], [3, 4]], index=index1, columns=[""A"", ""B""])

index2 = pd.Index(['L', 'M'], name=""Class"")
df2 = pd.DataFrame([[5, 6], [7, 8]], index=index2, columns=[""A"", ""B""])

index3 = pd.Index(['R', 'S'], name=""Class"")
df3 = pd.DataFrame([[9, 10], [11, 12]], index=index3, columns=[""A"", ""B""])

concatenated = pd.concat([df1, df2, df3], keys=['df1', 'df2', 'df3'], names=['source_df'])
");

                using var df1 = test.GetAttr("df1");
                using var df2 = test.GetAttr("df2");
                using var df3 = test.GetAttr("df3");
                using var expected = test.GetAttr("concatenated");

                using var concatenated = PandasConverter.ConcatDataFrames(new[] { df1, df2, df3 },
                    keys: new[] { "df1", "df2", "df3" },
                    names: new[] { "source_df" },
                    sort: false,
                    dropna: false);

                Assert.AreEqual(expected.GetAttr("to_string").Invoke().GetAndDispose<string>(),
                    concatenated.GetAttr("to_string").Invoke().GetAndDispose<string>());
            }
        }

        [Test]
        public void ConcatenatesDataFramesWithAddedMultiIndexLevel()
        {
            using (Py.GIL())
            {
                var test = PyModule.FromString("ConcatenatesDataFramesWithAddedMultiIndexLevel",
                    @"
import pandas as pd

index1 = pd.Index(['X', 'Y'], name='Class')
df1 = pd.DataFrame([[1, 2], [3, 4]], index=index1, columns=['A', 'B'])

index2 = pd.Index(['L', 'M'], name='Class')
df2 = pd.DataFrame([[5, 6], [7, 8]], index=index2, columns=['A', 'B'])

index3 = pd.Index(['R', 'S'], name='Class')
df3 = pd.DataFrame([[9, 10], [11, 12]], index=index3, columns=['A', 'B'])

index4 = pd.Index(['A', 'B'], name='Class')
df4 = pd.DataFrame([[11, 12], [13, 14]], index=index4, columns=['A', 'B'])

concatenated = pd.concat([df1, df2, df3, df4],
                         keys=[('Category 1', 'DF1'), ('Category 1', 'DF2'), ('Category 2', 'DF3'), ('Category 2', 'DF4')],
                         names=['Category', 'Source DF'])
");

                using var df1 = test.GetAttr("df1");
                using var df2 = test.GetAttr("df2");
                using var df3 = test.GetAttr("df3");
                using var df4 = test.GetAttr("df4");
                using var expected = test.GetAttr("concatenated");

                using var concatenated = PandasConverter.ConcatDataFrames(new[] { df1, df2, df3, df4 },
                    keys: new[] {
                        new[] { "Category 1", "DF1" },
                        new[] { "Category 1", "DF2" },
                        new[] { "Category 2", "DF3" },
                        new[] { "Category 2", "DF4" },
                    },
                    names: new[] { "Category", "Source DF" },
                    sort: false,
                    dropna: false);

                using var index = concatenated.GetAttr("index");
                using var getLevelValues = index.GetAttr("get_level_values");
                var level1 = getLevelValues.Invoke(0).GetAndDispose<string[]>();
                var level2 = getLevelValues.Invoke(1).GetAndDispose<string[]>();
                var level3 = getLevelValues.Invoke(2).GetAndDispose<string[]>();



                CollectionAssert.AreEqual(
                    new[] { "Category 1", "Category 1", "Category 1", "Category 1", "Category 2", "Category 2", "Category 2", "Category 2" },
                    level1);
                CollectionAssert.AreEqual(new[] { "DF1", "DF1", "DF2", "DF2", "DF3", "DF3", "DF4", "DF4" }, level2);
                CollectionAssert.AreEqual(new[] { "X", "Y", "L", "M", "R", "S", "A", "B" }, level3);

                Assert.AreEqual(expected.GetAttr("to_string").Invoke().GetAndDispose<string>(),
                    concatenated.GetAttr("to_string").Invoke().GetAndDispose<string>());
            }
        }

        [Test]
        public void ConcatenateReturnsEmptyDataFrameIfInputListIsEmpty()
        {
            using (Py.GIL())
            {
                var test = PyModule.FromString("ConcatenateReturnsEmptyDataFrameIfInputListIsEmpty",
                    @"
import pandas as pd

index1 = pd.Index(['X', 'Y'], name=""Class"")
df1 = pd.DataFrame([[1, 2], [3, 4]], index=index1, columns=[""A"", ""B""])

index2 = pd.Index(['L', 'M'], name=""Class"")
df2 = pd.DataFrame([[5, 6], [7, 8]], index=index2, columns=[""A"", ""B""])

index3 = pd.Index(['R', 'S'], name=""Class"")
df3 = pd.DataFrame([[9, 10], [11, 12]], index=index3, columns=[""A"", ""B""])

concatenated = pd.concat([df1, df2, df3], keys=['df1', 'df2', 'df3'], names=['source_df'])
");

                using var df1 = test.GetAttr("df1");
                using var df2 = test.GetAttr("df2");
                using var df3 = test.GetAttr("df3");
                using var expected = test.GetAttr("concatenated");

                using var concatenated = PandasConverter.ConcatDataFrames(Array.Empty<PyObject>());

                Assert.IsTrue(concatenated.GetAttr("empty").GetAndDispose<bool>());
            }
        }

        [Test]
        public void DataFrameDoesNotContainDefaultDateTimes()
        {
            using (Py.GIL())
            {
                var dateTime = new DateTime(2018, 1, 1);
                Func<int, IndicatorDataPoint> makePoint = i => new IndicatorDataPoint(dateTime.AddMinutes(i), i);
                var indicatorDataPoints = Enumerable.Range(0, 10).Select(i => makePoint(i)).ToList();
                // Add a default(DateTime) entry to test filtering
                indicatorDataPoints.Add(new IndicatorDataPoint(default, 1));

                var pdConverter = new PandasConverter();
                dynamic dataFrame = pdConverter.GetIndicatorDataFrame(new Dictionary<string, List<IndicatorDataPoint>>
                {
                    {"ind", indicatorDataPoints},
                });
                var index = dataFrame.index;
                foreach (dynamic time in index)
                {
                    DateTime timestamp = (DateTime)time.AsManagedObject(typeof(DateTime));
                    // Ensure the DataFrame index does not include default(DateTime)
                    Assert.AreNotEqual(default(DateTime), timestamp);
                }
            }
        }

        public IEnumerable<Slice> GetHistory<T>(Symbol symbol, Resolution resolution, IEnumerable<T> data)
            where T : IBaseData
        {
            var subscriptionDataConfig = GetSubscriptionDataConfig<T>(symbol, resolution);
            var security = GetSecurity(subscriptionDataConfig);
            var timeSliceFactory = new TimeSliceFactory(TimeZones.Utc);
            return data.Select(t => timeSliceFactory.Create(
               t.Time,
               new List<DataFeedPacket> { new DataFeedPacket(security, subscriptionDataConfig, new List<BaseData>() { t as BaseData }) },
               SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>()),
                new Dictionary<Universe, BaseDataCollection>()).Slice);
        }

        private SubscriptionDataConfig GetSubscriptionDataConfig<T>(Symbol symbol, Resolution resolution)
        {
            return new SubscriptionDataConfig(
                typeof(T),
                symbol,
                resolution,
                TimeZones.Utc,
                TimeZones.Utc,
                true,
                true,
                false);
        }

        private Security GetSecurity(SubscriptionDataConfig subscriptionDataConfig)
        {
            return new Security(
                SecurityExchangeHours.AlwaysOpen(TimeZones.Utc),
                subscriptionDataConfig,
                new Cash(Currencies.USD, 0, 1m),
                SymbolProperties.GetDefault(Currencies.USD),
                ErrorCurrencyConverter.Instance,
                RegisteredSecurityDataTypesProvider.Null,
                new SecurityCache()
            );
        }

        private dynamic GetTestDataFrame(Symbol symbol, int count = 1)
        {
            return GetTestDataFrame(new[] { symbol }, count);
        }

        private dynamic GetTestDataFrame(IEnumerable<Symbol> symbols, int count = 1)
        {
            var slices = Enumerable
                .Range(0, count)
                .Select(
                    i =>
                    {
                        var time = new DateTime(2013, 10, 7).AddMilliseconds(14400000 + i * 10000);
                        return new Slice(
                            time,
                            symbols.Select(
                                symbol => new Tick
                                {
                                    Time = time,
                                    Symbol = symbol,
                                    Value = 167 + i / 10,
                                    Quantity = 1 + i * 10,
                                    Exchange = "T"
                                }
                            ), time
                        );
                    }
                );
            return _converter.GetDataFrame(slices);
        }

        /// <summary>
        /// Test cases to verify that the <see cref="PandasConverter"/> handles slices with duplicate time stamps.
        /// We want to verify we handle non-unique multi-index errors from Pandas as described in GH issue #4297
        /// </summary>
        private static TestCaseData[] GetHistoryWithDuplicateTimes()
        {
            var time = new DateTime(2013, 10, 8);
            var symbol = Symbols.SPY;

            return new[]
            {
                // Trade, quote and open interest ticks
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, symbol, "04000001", "Q", 100, 1m), // trade
                                new Tick(time, symbol, "04000002", "P", 100m, 110m, 150m, 120m), // quote
                                new OpenInterest(time, symbol, 150m) // open interest
                            },
                            time
                        )
                    },
$"                             askprice  asksize  bidprice  bidsize exchange  lastprice  openinterest  quantity{Environment.NewLine}" +
$"symbol           time                                                                                        {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08       NaN      NaN       NaN      NaN   NASDAQ        1.0           NaN     100.0{Environment.NewLine}" +
$"                 2013-10-08     120.0    150.0     110.0    100.0     ARCA      115.0           NaN       0.0{Environment.NewLine}" +
$"                 2013-10-08       NaN      NaN       NaN      NaN                 NaN         150.0       0.0"
                ),
                // Trade tick
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, symbol, "04000001", "Q", 100, 1m), // trade
                            },
                            time
                        )
                    },
$"                            exchange  lastprice  quantity{Environment.NewLine}" +
$"symbol           time                                    {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08   NASDAQ        1.0     100.0"
                ),
                // Trade ticks with same timestamp
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, symbol, "04000001", "Q", 100, 1m), // trade
                                new Tick(time, symbol, "04000001", "Q", 200, 2m), // trade
                            },
                            time
                        )
                    },
$"                            exchange  lastprice  quantity{Environment.NewLine}" +
$"symbol           time                                    {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08   NASDAQ        1.0     100.0{Environment.NewLine}" +
$"                 2013-10-08   NASDAQ        2.0     200.0"
                ),
                // Quote tick
                new TestCaseData(
                    Symbols.BTCUSD,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, Symbols.BTCUSD, "04000002", "P", 100m, 110m, 150m, 120m), // quote
                            },
                            time
                        )
                    },
$"                       askprice  asksize  bidprice  bidsize  lastprice{Environment.NewLine}" +
$"symbol     time                                                       {Environment.NewLine}" +
$"BTCUSD 2XR 2013-10-08     120.0    150.0     110.0    100.0      115.0"
                ),
                // Quote ticks with same timestamp
                new TestCaseData(
                    Symbols.BTCUSD,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, Symbols.BTCUSD, "04000002", "P", 100m, 110m, 150m, 120m), // quote
                                new Tick(time, Symbols.BTCUSD, "04000002", "P", 200m, 210m, 250m, 220m), // quote
                            },
                            time
                        )
                    },
$"                       askprice  asksize  bidprice  bidsize  lastprice{Environment.NewLine}" +
$"symbol     time                                                       {Environment.NewLine}" +
$"BTCUSD 2XR 2013-10-08     120.0    150.0     110.0    100.0      115.0{Environment.NewLine}" +
$"           2013-10-08     220.0    250.0     210.0    200.0      215.0"
                ),
                // Open interest tick
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new OpenInterest(time, symbol, 150m) // open interest
                            },
                            time
                        )
                    },
$"                             openinterest{Environment.NewLine}" +
$"symbol           time                    {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08         150.0"
                ),
                // Open interest ticks with same timestamp
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new OpenInterest(time, symbol, 150m), // open interest
                                new OpenInterest(time, symbol, 250m) // open interest
                            },
                            time
                        )
                    },
$"                             openinterest{Environment.NewLine}" +
$"symbol           time                    {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08         150.0{Environment.NewLine}" +
$"                 2013-10-08         250.0"
                ),
                // Trade, quote and open interest ticks with different times
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new[]
                            {
                                new Tick(time, symbol, "04000001", "Q", 100, 1m), // trade
                                new Tick(time.AddTicks(1000000), symbol, "04000002", "P", 100m, 110m, 150m, 120m), // quote
                                new OpenInterest(time.AddSeconds(2 * 1000000), symbol, 150m) // open interest
                            },
                            time
                        )
                    },
$"                                          askprice  asksize  bidprice  bidsize exchange  lastprice  openinterest  quantity{Environment.NewLine}" +
$"symbol           time                                                                                                     {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08 00:00:00.000       NaN      NaN       NaN      NaN   NASDAQ        1.0           NaN     100.0{Environment.NewLine}" +
$"                 2013-10-08 00:00:00.100     120.0    150.0     110.0    100.0     ARCA      115.0           NaN       0.0{Environment.NewLine}" +
$"                 2013-10-31 03:33:20.000       NaN      NaN       NaN      NaN                 NaN         150.0       0.0"
                ),
                // Trade and quote bars
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new BaseData[]
                            {
                                new TradeBar(time, symbol, 101m, 102m, 100m, 101m, 10m),
                                new QuoteBar(time, symbol, new Bar(101m, 102m, 100m, 101m), 99m, new Bar(110m, 112m, 105m, 110m), 98m)
                            },
                            time
                        )
                    },
$"                                      askclose  askhigh  asklow  askopen  asksize  bidclose  bidhigh  bidlow  bidopen  bidsize  close   high    low   open  volume{Environment.NewLine}" +
$"symbol           time                                                                                                                                             {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08 00:01:00     110.0    112.0   105.0    110.0     98.0     101.0    102.0   100.0    101.0     99.0  101.0  102.0  100.0  101.0    10.0"
                ),
                // Trade bar
                new TestCaseData(
                    symbol,
                    new[]
                    {
                        new Slice(
                            time,
                            new BaseData[]
                            {
                                new TradeBar(time, symbol, 101m, 102m, 100m, 101m, 10m)
                            },
                            time
                        )
                    },
$"                                      close   high    low   open  volume{Environment.NewLine}" +
$"symbol           time                                                   {Environment.NewLine}" +
$"SPY R735QTJ8XC9X 2013-10-08 00:01:00  101.0  102.0  100.0  101.0    10.0"
                ),
                // Quote bar
                new TestCaseData(
                    Symbols.BTCUSD,
                    new[]
                    {
                        new Slice(
                            time,
                            new BaseData[]
                            {
                                new QuoteBar(time, Symbols.BTCUSD, new Bar(101m, 102m, 100m, 101m), 99m, new Bar(110m, 112m, 105m, 110m), 98m)
                            },
                            time
                        )
                    },
$"                                askclose  askhigh  asklow  askopen  asksize  bidclose  bidhigh  bidlow  bidopen  bidsize  close   high    low   open{Environment.NewLine}" +
$"symbol     time                                                                                                                                     {Environment.NewLine}" +
$"BTCUSD 2XR 2013-10-08 00:01:00     110.0    112.0   105.0    110.0     98.0     101.0    102.0   100.0    101.0     99.0  105.5  107.0  102.5  105.5"
                ),
                // Trade and quote bars with different times
                new TestCaseData(
                    Symbols.BTCUSD,
                    new[]
                    {
                        new Slice(
                            time,
                            new BaseData[]
                            {
                                new TradeBar(time, Symbols.BTCUSD, 101m, 102m, 100m, 101m, 10m),
                            },
                            time
                        ),
                        new Slice(
                            time.AddMinutes(1),
                            new BaseData[]
                            {
                                new QuoteBar(time.AddMinutes(1).AddSeconds(1), Symbols.BTCUSD, new Bar(101m, 102m, 100m, 101m), 99m, new Bar(110m, 112m, 105m, 110m), 98m)
                            },
                            time.AddMinutes(1))
                    },
$"                                askclose  askhigh  asklow  askopen  asksize  bidclose  bidhigh  bidlow  bidopen  bidsize  close   high    low   open  volume{Environment.NewLine}" +
$"symbol     time                                                                                                                                             {Environment.NewLine}" +
$"BTCUSD 2XR 2013-10-08 00:01:00       NaN      NaN     NaN      NaN      NaN       NaN      NaN     NaN      NaN      NaN  101.0  102.0  100.0  101.0    10.0{Environment.NewLine}" +
$"           2013-10-08 00:02:01     110.0    112.0   105.0    110.0     98.0     101.0    102.0   100.0    101.0     99.0  105.5  107.0  102.5  105.5     NaN"
                ),
            };
        }

        internal class SubTradeBar : TradeBar
        {
            public decimal SubProperty => Value;

            public SubTradeBar() { }

            public SubTradeBar(TradeBar tradeBar) : base(tradeBar) { }

            public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode) =>
                new SubTradeBar((TradeBar)base.Reader(config, line, date, isLiveMode));
        }

        internal class SubSubTradeBar : SubTradeBar
        {
            public decimal SubSubProperty => Value;

            public SubSubTradeBar() { }

            public SubSubTradeBar(TradeBar tradeBar) : base(tradeBar) { }

            public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode) =>
                new SubSubTradeBar((TradeBar)base.Reader(config, line, date, isLiveMode));
        }

        internal class NullableValueData : BaseData
        {
            public int? NullableInt { get; set; }

            public DateTime? NullableTime { get; set; }

            public double? NullableColumn { get; set; }
        }

        internal class CustomData : DynamicData
        {
            private bool _isInitialized;
            private readonly List<string> _propertyNames = new List<string>();

            public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode)
            {
                // be sure to instantiate the correct type
                var data = (CustomData)Activator.CreateInstance(GetType());
                data.Symbol = config.Symbol;
                var csv = line.Split(',');

                if (!_isInitialized)
                {
                    _isInitialized = true;
                    foreach (var propertyName in csv)
                    {
                        var property = propertyName.Trim();
                        // should we remove property names like Time?
                        // do we need to alias the Time??
                        data.SetProperty(property, 0m);
                        _propertyNames.Add(property);
                    }
                    // Returns null at this point where we are only reading the properties names
                    return null;
                }

                data.Time = DateTime.ParseExact(csv[0], "yyyy-MM-dd", CultureInfo.InvariantCulture);

                for (var i = 1; i < csv.Length; i++)
                {
                    var value = csv[i].ToDecimal();
                    data.SetProperty(_propertyNames[i], value);
                }

                if (!_propertyNames.Contains("Value"))
                {
                    data.Value = 1;
                }

                return data;
            }
        }

        internal class EnumerableData : BaseDataCollection
        {

        }
    }
}
